Skip to content

fix(build): emit spec .d.ts in program order so two builds of one sha match byte for byte - #30131

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-v0qas7
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-v0qas7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Part of #30061 (the seat closes the card on landing; a claude/project-thread-* branch cannot satisfy check:closing-target-claim).

维护者速读

  • 改了什么:tsup 补丁里的 rollup-plugin-dts 改为在 rollup 读取任何模块之前,按 ts.Program 自身的文件顺序把每个源文件的 .d.ts 一次性生成好;另加一个 pin 测试。spec 源码与 schema 一行未动。
  • 为什么改:同一份 main 源码构建两次,spec 的 134 个 .d.ts 中有 18 个字节不同(含 chunk 文件名),拆分大文件的 PR 因此只能两两配对读声明证据。根因是生成顺序跟着 rollup 的异步加载顺序走,每次都不一样。
  • 风险与回滚:补丁作用于仓内所有走 tsup DTS 的包;每个文件仍只生成一次,spec 的 DTS 耗时 44s 不变。修复后的产物与 main 已经会产出的两种状态之一逐字节相同。回滚 = revert 本 PR(补丁、锁文件哈希一起回)。
  • 先看:patches/tsup@8.5.1.patch(新增的 emitInProgramOrder 与 transform 钩子里的读取);packages/spec/scripts/dts-emit-order.pin.test.ts;packages/spec/tsup.config.ts 中改写的文档块。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --project local scripts/dts-emit-order.pin.test.ts,预期 2 个测试通过,约 5 秒。
  • 需要你做的:无,席位落地。

Clause-②: no
Metadata-protocol: none

What changes

The cause. @objectstack/spec's DTS pass runs every entry through one ts.Program (the existing tsup patch). Its checker creates each type once, on first use, and TypeScript prints union members, and the object members derived from them, in type-creation order. tsup's bundled rollup-plugin-dts emitted each file's declarations inside rollup's transform hook, so whichever file rollup transformed first created the shared literal types and fixed the member order of every later file. Rollup transforms in the order its async fs.promises.readFile loads finish, which changes from run to run. Measured: in two DTS passes of one tree the transform order differed at 389 of 445 positions, starting with the first module.

The parallel-build suspect does not hold. tsup runs the DTS pass as ONE rollup build in one worker, transforms each module once, and writes the esm and cjs outputs from that one module graph. The .d.ts and .d.mts twins carry the same member order within a run. The order moves between runs, not between the formats.

The fix. patches/tsup@8.5.1.patch (regenerated with pnpm patch / pnpm patch-commit; the lockfile's patch_hash moves with it) adds emitInProgramOrder(program): on the first transform, it emits every non-declaration, non-external source file of the program once, in program.getSourceFiles() order, and stores each file's text and diagnostics. The transform hook then reads the stored text instead of emitting. Each file is still emitted exactly once, and the noEmitOnError error path is unchanged. The first hunk, which gives every entry one program, is unchanged.

Docs. packages/spec/tsup.config.ts's "NOT BYTE-STABLE" paragraph is rewritten: one tree now gives one set of bytes, but the order is the program's, so comparing two DIFFERENT sources still needs the order-insensitive reading. pnpm-workspace.yaml's note on the tsup patch gains the second change.

The pin. packages/spec/scripts/dts-emit-order.pin.test.ts (local project, ~4 s) builds a two-entry fixture through the real tsup CLI twice. A preload delays rollup's read of a.ts in one run and b.ts in the other, so the two runs transform the entries in opposite orders, and the test requires byte-equal output (.d.ts and .d.mts). Two controls: the fixture is order-sensitive under this TypeScript (a hand emit in the two orders gives "zeta" | "alpha" | "mid" and "mid" | "alpha" | "zeta"), and each run's read log shows the forced order reached rollup.

Why a fixture and not two full builds in CI: one spec DTS pass is ~45 s and ~3.5 GB RSS, and of four unforced builds of main, three agreed byte for byte. Two full builds would double the cost and still miss a regression most of the time. The forced order fails on every run.

Its radius, measured: a commit changing only patches/tsup@8.5.1.patch and pnpm-lock.yaml makes turbo ls --affected report 131 packages, @objectstack/spec among them, and the root external-dependency hash is in turbo's global cache inputs. So a later tsup patch change re-runs this pin.

Changeset: skip-changeset. Nothing published changes. The post-fix spec declaration tree is byte-identical, 134/134, to one of the two trees main already builds (base1 below). tsup.config.ts, the test, patches/ and the lockfile are not in any files[].

Tests

  • Pin: 2/2 pass. Ablation: with the installed rollup.js swapped for the pre-fix one (one-program hunk only), the byte-equality test fails (expected { …(4) } to deeply equal { …(4) }) and the order-sensitivity control still passes. Restored, sha256 checked equal to the fixed file.
  • pnpm --filter @objectstack/spec test: exit 0, 812/812 files, 24397 passed, 1 todo.
  • NODE_OPTIONS=--max-old-space-size=6144 pnpm --filter @objectstack/spec typecheck: exit 0.
  • eslint --no-inline-config on the two touched TypeScript files: exit 0.

Acceptance notes

  • The patch serves every package that builds declarations with tsup here (twelve have more than one DTS entry). Their declaration member order now follows their program order too. Equivalent types, possibly a different one of their existing run-to-run orders on their next release. NOT MEASURED per package: reason, outside this card's spec measurement; no published file of theirs changes in this PR.
  • File surface: the claim named the spec d.ts build configuration under packages/spec. The rollup-plugin-dts behaviour lives in the root tsup patch, so the diff also touches patches/tsup@8.5.1.patch, pnpm-lock.yaml (the patch hash) and pnpm-workspace.yaml (comment only).
  • A move-only split now reads from ONE base/head pair. Measured on refactor(spec): codemod split of conversions/registry.ts into per-entry modules (#29987) #29993's split: the tree with d79eecfe's packages/spec/src change reverted and the current tree gave 134/134 byte-identical declarations under the fix.
  • The --dts check in scripts/codemod-split-*.mjs is untouched (Codemod split: put driver-sql sql-driver.ts into facade form with a deterministic script, like protocol.ts and engine.ts #30105 is in flight there).

Verification

Spec builds, before and after (raw sha256 per declaration file)

All on the tree of 1f13eb31 (current main), rm -rf dist then pnpm build per run; per-file sha256 lists in the project files under issue-30061/.

Pair Differing of 134
base1 vs base2 (before) 18
two DTS-only passes before the fix (o1, o2) 0, both equal to base1
head1 vs head2 (after) 0
head1 vs base1 0
head1 vs base2 18
three DTS-only passes after the fix (f1, f2, f3) 0, while the transform order differed in each

The 18 differing files before the fix: ai, api, automation, contracts, kernel, marketplace, system (index.d.ts and .d.mts each), and the automation-api.zod-HASH and node-executor.zod-HASH chunks (both formats, renamed between runs).

Surface reading (dtsnorm.mjs from issue-29987/, base2 vs head1): same file set, 116 raw-equal, 18 differ only in union order, surface digest EQUAL in all 18. The fix moves no published surface; it picks one of main's existing orders every time.

DTS pass wall time: 44-47 s before, 43-47 s after (shared box).

Derived gates

All on head 6d4463b4. node scripts/pm/dispatch-gates.mjs --commands derived 66 families for this change set (the lockfile pulls in the wide set); each was run and its exit code recorded before any pipe (issue-30061/gates-ran.list in the project files).

  • 63 exit 0, among them check:cross-package-test-inputs, check:test-source-alias, check:nul-bytes, check:published-files, check:dts-closure (38/38 declared declaration files present), check:type-check-coverage, check:type-check-debt, check:ci-filter-parity, check:pm-governed-merges and spec's check:liveness, check:llms-txt, check:strictness-ledger, check:variant-docs, check:empty-state.
  • 3 NOT MEASURED, exit 3 (the gate refused its own prerequisite): check-plugin-teardown-shape.mjs --self-test (shallow clone, wants git fetch --unshallow), check:dual-build-cjs-loads and check:lean-entry-closure (both want the whole workspace built; only packages/spec was). CI runs all three.
  • dispatch-gates --ran: exit 0, 66 derived, 63 run, 3 NOT-MEASURED, 0 UNRUN.
  • check-dev-prereqs (local-only family): NOT MEASURED, exit 1 because 35 other workspace packages are unbuilt in this container; spec's own build stamped its prerequisites (check-dev-prereqs --stamp inside pnpm build, exit 0).

Generated by Claude Code

…h byte for byte

tsup's bundled rollup-plugin-dts emitted each declaration file when rollup
transformed it. Rollup transforms in the order its async loads finish, which
changes from run to run, and TypeScript prints union members in the order the
one shared program's checker created them. Two clean builds of one commit of
@objectstack/spec therefore differed in 18 of 134 .d.ts files, chunk names
included, with the same public surface.

The tsup patch now emits every source file once, in the program's own file
order, before rollup reads any; the transform hook reads the stored text.
Each file is still emitted exactly once.

A pin builds a two-entry fixture through the real tsup CLI twice, forcing
opposite load orders, and requires byte-equal output. Its controls prove the
fixture is order-sensitive under this TypeScript and that the forced order
reached rollup's loader.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzeza8Qg2GQUtJQtxRW
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 4, 2026
@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/tsup.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/tsup.config.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 28ce3d1bfa9060035c75c65612f9f9452e28a378 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-support-ai · session_01WGufzgBDcBNVmSmo7VFHpj

Stamp: 2026-11-04T15:17Z
Head-sha: 6d4463b

维护者速读

  • 改了什么: 扩展仓库已有的 patches/tsup@8.5.1.patch:生成 .d.ts 时,先按 TypeScript 程序自身的文件顺序把每个源文件各生成一次,再交给 rollup,不再按 rollup 异步加载完成的先后顺序逐个生成。新增 pin 测试 packages/spec/scripts/dts-emit-order.pin.test.ts。
  • 为什么改: 同一份源码构建两次,@objectstack/spec 会写出不同的 .d.ts 字节和 chunk 名(在 1f13eb3 上 134 个文件里有 18 个不同)。拆分 PR 的「声明文件不变」证明因此只能构建四次两两配对来读(spec build: two builds of the same main source write different .d.ts files (10 files, chunk hash rename) #30061)。
  • 风险与回滚: 发布内容不变:修复后的 head 与修复前的 base 逐字节相同,surface EQUAL。补丁对所有用 tsup 生成 DTS 的包都生效,只改生成顺序,每个文件仍只生成一次。DTS 耗时 44 秒,没有变化。回滚就是 revert。
  • 先看: 补丁里的 emitInProgramOrder,以及 pin 测试的两个对照。
  • 两分钟验证: 在 spec 包里跑 pin 测试:同一提交强制两种相反的加载顺序,两次产出逐字节相同。
  • 需要你做的: 无,由席位落地。
  • 席位意见: ACCEPT。根因找到了,修在生成顺序这一处。今后拆分 PR 读一对构建就能证明声明文件不变。

席位核对

  • 形态: 5 个文件,+295/−27,非受管面(check-governed-merges.mjs --pr 30131 判 NOT governed,322 行,不超过 3000),check-owed-reviews 不欠复核。
  • 前提实测: dev 读数:base 两次构建有 18/134 个文件不同;head 两次构建 0/134 不同;head1 与 base1 逐字节相同;dtsnorm surface EQUAL;把 refactor(spec): codemod split of conversions/registry.ts into per-entry modules (#29987) #29993 的拆分回退后与当前比较,134/134 相同。根因是并行之外的另一处:一次 rollup 构建、一个 worker 内,transform hook 的先后顺序不固定。席位读了补丁:未进有序表的文件回落到原来的逐文件 emit,同一文件不会被转换两次。
  • 门禁 check-runs: 钉 6d4463b4,席位读数为 11 success、12 skipped、24 in_progress,auto-merge 会等全绿。dev 本地:spec test 812/812、typecheck 0、eslint 0;dispatch-gates 66 条中 63 条 exit 0,3 条 exit 3 NOT MEASURED(浅克隆或工作区未构建,交 CI 覆盖)。pin 测试已做消融:恢复修复前的 rollup.js 后变红。
  • 接受的偏差: 改动超出认领面 packages/spec,触及 patches/tsup@8.5.1.patch、pnpm-lock.yaml 和 pnpm-workspace.yaml(注释)。补丁本来就在那里,只能改那里。dev 未逐包测量其余 11 个用 tsup DTS 的包:顺序变了,内容等价,发布不受影响,所以 skip-changeset 成立。
  • 范围外去向: 无。今后拆分卡的 .d.ts 证明可以只读一对构建。Skills-docs: none。
  • 落地条款: 席位 pr_ready + automerge_enable。合并后关卡 spec build: two builds of the same main source write different .d.ts files (10 files, chunk hash rename) #30061。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 15:19
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 4, 2026 15:19
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 2168447 Nov 4, 2026
71 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-v0qas7 branch November 4, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant